昨天將 todo-api 與 web 的共用設定和環境差異拆開了,但兩個服務仍要各自準備 Deployment、Service,並知道哪些 label、probe 與 selector 不能寫錯。文件可以說明規則,卻不能阻止一份不合規的 YAML 進入叢集。
先回頭看既有的 todo-api 與 web。它們真正要表達的可能只有:使用哪個 image、在哪個 port 提供服務。其餘 Kubernetes 細節不該每次從頭決定。Custom Resource Definition(CRD) 讓我們能把這種團隊語言加入 Kubernetes API,先定義一份可驗證的服務合約。
Kubernetes 原生知道 Deployment、Service、ConfigMap 等資源,卻不知道「可交付的微服務」代表什麼。安裝 CRD 後,API Server 會接受新的資源型別,例如 Microservice;它也會依 CRD 定義的 schema 檢查送進來的資料。
這裡有四個名稱很像、責任卻不同的角色:
todo-api。status。最後一點不能混在一起看。CRD 定義 Kubernetes「認得什麼資料」;Controller/Operator 負責「收到資料後要做什麼」。因此,安裝 CRD 後能執行 kubectl get microservices,不表示 todo-api 或 web 已經由 CR 部署。沒有 controller 的 CR,只是 API Server 保存的一筆資料。
平台自定義合約資源,不要把整個 Kubernetes PodSpec 原封不動塞進 CRD。這樣看似保留彈性,實際上只是替原生 YAML 換了一層名字,平台無法提供可靠預設,開發者也還是要學會所有低階選項。
Todo 系統的第一版只讓服務擁有者決定 image 與服務 port。副本策略、受管 label、健康檢查、預設 resource policy 與 telemetry 注入留給平台處理。這份 CRD 的核心 schema 如下:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: microservices.platform.example.io
spec:
group: platform.example.io
scope: Namespaced
names:
plural: microservices
singular: microservice
kind: Microservice
shortNames:
- ms
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- image
- port
properties:
image:
type: string
minLength: 1
port:
type: integer
minimum: 1
maximum: 65535
status:
type: object
properties:
phase:
type: string
observedGeneration:
type: integer
managedResources:
type: object
properties:
deployment:
type: string
service:
type: string
v1alpha1 表示合約仍可能演進,但不表示可以隨意破壞已存在的 CR。即使在 alpha 階段,也要考慮新增欄位是否相容、舊資料如何讀取,以及未來 version conversion 的成本。
todo-api 與 web 表達成服務意圖安裝 CRD 後,服務團隊提交的 todo-api 不再是一大段 Deployment:
apiVersion: platform.example.io/v1alpha1
kind: Microservice
metadata:
name: todo-api
namespace: todo
labels:
app.kubernetes.io/owner: todo-team
spec:
image: ghcr.io/yrw9281/it30-todo-api:0.1.0
port: 8080
web 使用相同合約,只差在 image 與 port:
apiVersion: platform.example.io/v1alpha1
kind: Microservice
metadata:
name: web
namespace: todo
labels:
app.kubernetes.io/owner: todo-team
spec:
image: ghcr.io/yrw9281/it30-todo-web:0.1.0
port: 80
spec 是服務擁有者提交的意圖:要使用哪個 image、在哪個 port 提供服務。以下狀態則由平台觀察後寫回,不應由服務擁有者手動宣稱:
# Operator 觀察後寫回的狀態範例
status:
observedGeneration: 4
phase: Progressing
managedResources:
deployment: todo-api
service: todo-api
啟用 status subresource 後,平台 controller 可以取得專門更新 status 的權限,而一般服務提交者只管理 spec。這個界線避免把「資源已提交」誤當成「服務已健康」。
在 status 設計上,早期常直接使用 phase 列舉狀態(如 Progressing、Ready),但現代 Kubernetes API 更推薦搭配 observedGeneration 與標準的 conditions 陣列。這樣既能表達詳細的健康狀態,也能讓使用者分辨 controller 是否已處理到最新的 spec。
將概念提升成 CRD 有成本。每個欄位都是未來要維護的 API 承諾。太早抽象,可能把尚未穩定的部署細節鎖死;太晚抽象,各團隊又會各自發明 YAML。適合做成 CRD 的能力,通常會跨多個服務重複出現、需要 controller 持續維持,而且有清楚的領域語意。
CRD 也不會取代 RBAC、Policy 或 Git review。它只能約束結構與欄位範圍;誰可以建立資源、image 是否來自受信任的 GHCR repository、何時能進 Production,仍需要另外定義治理規則。
先將 CRD 套用到叢集,再用 server-side dry run 驗證 API Server 是否接受 CR:
# 安裝資源型別;這一步會真的寫入叢集
kubectl apply -f src/1-kubernetes/crd/microservice-crd.yaml
# 兩個合法 CR 都應通過 schema 驗證,但不會產生 Deployment
kubectl apply --server-side --dry-run=server \
-f src/1-kubernetes/crd/todo-api.yaml \
-f src/1-kubernetes/crd/web.yaml
# port 為 65536,應在寫入前被 schema 拒絕
kubectl apply --server-side --dry-run=server \
-f src/1-kubernetes/crd/invalid-todo-api.yaml
todo-api 與 web 的 CR 可以通過驗證,卻不會自動產生 Deployment,因為 Operator 尚未實作。invalid-todo-api.yaml 的 port: 65536 超出 schema 的上限,API Server 應拒絕寫入。若輸入未定義的欄位,API Server 預設會因結構驗證不符而拒絕寫入;即使在允許剪裁(Pruning)的設定下被忽略,也不能把它誤解為驗證成功。
這組結果確認 CRD 是合約,不是自動化本身。要讓 Microservice CR 真的展開成 Kubernetes 原生資源,還需要由 controller 持續觀察 CR 並執行 reconcile。
CRD 讓 Kubernetes 開始理解平台自定義的領域語言,但它只定義服務意圖的形狀。每個 Microservice 變更仍需要可信的保存位置與審查機制,才能知道環境應維持哪一份設定,並在需要時追溯與重建,而我們明天會試著把 Git 當作單一事實來源(SSoT)。